// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MethodInvocationValidator.cs" company="">
//   
// </copyright>
// <summary>
//   This class is used to validate a method call (invocation) for method arguments.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace Skymate.Runtime.Validation.Interception
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.Reflection;

    using Skymate.Extensions;
    using Skymate.Utilities;

    /// <summary>
    ///     This class is used to validate a method call (invocation) for method arguments.
    /// </summary>
    internal class MethodInvocationValidator
    {
        /// <summary>
        ///     The _method.
        /// </summary>
        private readonly MethodInfo _method;

        /// <summary>
        ///     The _parameters.
        /// </summary>
        private readonly ParameterInfo[] _parameters;

        /// <summary>
        ///     The _parameter values.
        /// </summary>
        private readonly object[] _parameterValues;

        /// <summary>
        ///     The _validation errors.
        /// </summary>
        private readonly List<ValidationResult> _validationErrors;

        /// <summary>
        /// Initializes a new instance of the <see cref="MethodInvocationValidator"/> class.
        ///     Creates a new <see cref="MethodInvocationValidator"/> instance.
        /// </summary>
        /// <param name="method">
        /// Method to be validated
        /// </param>
        /// <param name="parameterValues">
        /// List of arguments those are used to call the <paramref name="method"/>.
        /// </param>
        public MethodInvocationValidator(MethodInfo method, object[] parameterValues)
        {
            this._method = method;
            this._parameterValues = parameterValues;
            this._parameters = method.GetParameters();
            this._validationErrors = new List<ValidationResult>();
        }

        /// <summary>
        ///     Validates the method invocation.
        /// </summary>
        public void Validate()
        {
            if (!this._method.IsPublic)
            {
                // Validate only public methods!
                return;
            }

            if (this._method.IsDefined(typeof(DisableValidationAttribute)))
            {
                // Don't validate if explicitly requested!
                return;
            }

            if (this._parameters.IsNullOrEmpty())
            {
                // Object has no parameter, no need to validate.
                return;
            }

            if (this._parameters.Length != this._parameterValues.Length)
            {
                // This is not possible actually
                throw new Exception("Method parameter count does not match with argument count!");
            }

            for (var i = 0; i < this._parameters.Length; i++)
            {
                this.ValidateMethodParameter(this._parameters[i], this._parameterValues[i]);
            }

            if (this._validationErrors.Any())
            {
                throw new SkymateValidationException(
                    "Method arguments are not valid! See ValidationErrors for details.", 
                    this._validationErrors);
            }

            foreach (var parameterValue in this._parameterValues)
            {
                NormalizeParameter(parameterValue);
            }
        }

        /// <summary>
        /// Validates given parameter for given value.
        /// </summary>
        /// <param name="parameterInfo">
        /// Parameter of the method to validate
        /// </param>
        /// <param name="parameterValue">
        /// Value to validate
        /// </param>
        private void ValidateMethodParameter(ParameterInfo parameterInfo, object parameterValue)
        {
            if (parameterValue == null)
            {
                if (!parameterInfo.IsOptional && !parameterInfo.IsOut
                    && !TypeHelper.IsPrimitiveExtendedIncludingNullable(parameterInfo.ParameterType))
                {
                    this._validationErrors.Add(
                        new ValidationResult(parameterInfo.Name + " is null!", new[] { parameterInfo.Name }));
                }

                return;
            }

            this.ValidateObjectRecursively(parameterValue);
        }

        /// <summary>
        /// The validate object recursively.
        /// </summary>
        /// <param name="validatingObject">
        /// The validating object.
        /// </param>
        private void ValidateObjectRecursively(object validatingObject)
        {
            if (validatingObject is IEnumerable && !(validatingObject is IQueryable))
            {
                foreach (var item in validatingObject as IEnumerable)
                {
                    this.ValidateObjectRecursively(item);
                }
            }

            if (!(validatingObject is IValidate))
            {
                return;
            }

            this.SetValidationAttributeErrors(validatingObject);

            if (validatingObject is ICustomValidate)
            {
                (validatingObject as ICustomValidate).AddValidationErrors(this._validationErrors);
            }

            var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();
            foreach (var property in properties)
            {
                this.ValidateObjectRecursively(property.GetValue(validatingObject));
            }
        }

        /// <summary>
        /// Checks all properties for DataAnnotations attributes.
        /// </summary>
        /// <param name="validatingObject">
        /// The validating Object.
        /// </param>
        private void SetValidationAttributeErrors(object validatingObject)
        {
            var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();
            foreach (var property in properties)
            {
                var validationAttributes = property.Attributes.OfType<ValidationAttribute>().ToArray();
                if (validationAttributes.IsNullOrEmpty())
                {
                    continue;
                }

                var validationContext = new ValidationContext(validatingObject)
                                            {
                                                DisplayName = property.Name, 
                                                MemberName = property.Name
                                            };

                foreach (var attribute in validationAttributes)
                {
                    var result = attribute.GetValidationResult(property.GetValue(validatingObject), validationContext);
                    if (result != null)
                    {
                        this._validationErrors.Add(result);
                    }
                }
            }
        }

        /// <summary>
        /// The normalize parameter.
        /// </summary>
        /// <param name="parameterValue">
        /// The parameter value.
        /// </param>
        private static void NormalizeParameter(object parameterValue)
        {
            if (parameterValue is IShouldNormalize)
            {
                (parameterValue as IShouldNormalize).Normalize();
            }
        }
    }
}